{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56db45b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp components"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35d7e3af",
   "metadata": {},
   "source": [
    "# Components\n",
    "> `ft_html` and `ft_hx` functions to add some conveniences to `ft`, along with a full set of basic HTML components, and functions to work with forms and `FT` conversion"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e2d405b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING\n",
    "from bs4 import BeautifulSoup, Comment\n",
    "\n",
    "from fastcore.utils import *\n",
    "from fastcore.xml import *\n",
    "from fastcore.meta import use_kwargs, delegates\n",
    "from fastcore.test import *\n",
    "from fasthtml.core import fh_cfg\n",
    "\n",
    "import types, json\n",
    "\n",
    "try: from IPython import display\n",
    "except ImportError: display=None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ccb463a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from lxml import html as lx\n",
    "from pprint import pprint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad9c6bf2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def show(ft,*rest):\n",
    "    \"Renders FT Components into HTML within a Jupyter notebook.\"\n",
    "    if rest: ft = (ft,)+rest\n",
    "    return display.HTML(to_xml(ft))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d1dcd4a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<p><strong>\n",
       "FastHTML is\n",
       "  <i>Fast</i>\n",
       "</strong>\n",
       "</p>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sentence = P(Strong(\"FastHTML is\", I(\"Fast\")))\n",
    "\n",
    "# When placed within the `show()` function, this will render\n",
    "# the HTML in Jupyter notebooks.\n",
    "show(sentence)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce7c9f92",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<p><strong>\n",
       "FastHTML is\n",
       "  <i>Fast</i>\n",
       "</strong>\n",
       "</p>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "p((strong(('FastHTML is', i(('Fast',),{})),{}),),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Called without the `show()` function, the raw HTML is displayed\n",
    "sentence"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ff9acc3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "named = set('a button form frame iframe img input map meta object param select textarea'.split())\n",
    "html_attrs = 'id cls title style accesskey contenteditable dir draggable enterkeyhint hidden inert inputmode lang popover spellcheck tabindex translate'.split()\n",
    "hx_attrs = 'get post put delete patch trigger target swap swap_oob include select select_oob indicator push_url confirm disable replace_url vals disabled_elt ext headers history history_elt indicator inherit params preserve prompt replace_url request sync validate'\n",
    "hx_attrs = html_attrs + [f'hx_{o}' for o in hx_attrs.split()]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f904f825",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def attrmap_x(o):\n",
    "    if o.startswith('_at_'): o = '@'+o[4:]\n",
    "    return attrmap(o)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ee041be0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "fh_cfg['attrmap']=attrmap_x\n",
    "fh_cfg['valmap' ]=valmap"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8ade6b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ft_html(tag: str, *c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, **kwargs):\n",
    "    if attrmap is None: attrmap=fh_cfg.attrmap\n",
    "    if valmap  is None: valmap =fh_cfg.valmap\n",
    "    kwargs['id'],kwargs['cls'],kwargs['title'],kwargs['style'] = id,cls,title,style\n",
    "    tag,c,kw = ft(tag, *c, attrmap=attrmap, valmap=valmap, **kwargs).list\n",
    "    if tag in named and 'id' in kw and 'name' not in kw: kw['name'] = kw['id']\n",
    "    return FT(tag,c,kw, void_=tag in voids)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5158b3d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@use_kwargs(hx_attrs, keep=True)\n",
    "def ft_hx(tag: str, *c, target_id=None, hx_vals=None, **kwargs):\n",
    "    if hx_vals: kwargs['hx_vals'] = json.dumps(hx_vals) if isinstance (hx_vals,dict) else hx_vals\n",
    "    if target_id: kwargs['hx_target'] = '#'+target_id\n",
    "    return ft_html(tag, *c, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9639f404",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a @click_dot_away=\"1\"></a>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "a((),{'@click_dot_away': 1})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', _at_click_dot_away=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "772a9405",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a @click.away=\"1\"></a>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "a((),{'@click.away': 1})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', **{'@click.away':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f40e7f4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a hx-vals='{\"a\": 1}'></a>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "a((),{'hx-vals': '{\"a\": 1}'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_hx('a', hx_vals={'a':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ede9b44d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "_g = globals()\n",
    "_all_ = [\n",
    "    'A', 'Abbr', 'Address', 'Area', 'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi', 'Bdo', 'Blockquote', 'Body', 'Br',\n",
    "    'Button', 'Canvas', 'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data', 'Datalist', 'Dd', 'Del', 'Details', 'Dfn',\n",
    "    'Dialog', 'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe', 'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form',\n",
    "    'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header',\n",
    "    'Hgroup', 'Hr', 'Html', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd', 'Label', 'Legend', 'Li',\n",
    "    'Link', 'Main', 'Map', 'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object', 'Ol', 'Optgroup', 'Option', 'Output',\n",
    "    'P', 'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp', 'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search',\n",
    "    'Section', 'Select', 'Slot', 'Small', 'Source', 'Span', 'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table', 'Tbody',\n",
    "    'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']\n",
    "for o in _all_: _g[o] = partial(ft_hx, o.lower())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ccf4371",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def File(fname):\n",
    "    \"Use the unescaped text in file `fname` directly\"\n",
    "    return NotStr(Path(fname).read_text())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b030be9",
   "metadata": {},
   "source": [
    "For tags that have a `name` attribute, it will be set to the value of `id` if not provided explicitly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ecbcfa18",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form hx-post=\"/\" hx-target=\"#tgt\" id=\"frm\" name=\"frm\"><button hx-target=\"#foo\" id=\"btn\" name=\"btn\"></button>\n",
       "</form>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "form((button((),{'hx-target': '#foo', 'id': 'btn', 'name': 'btn'}),),{'hx-post': '/', 'hx-target': '#tgt', 'id': 'frm', 'name': 'frm'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Form(Button(target_id='foo', id='btn'),\n",
    "     hx_post='/', target_id='tgt', id='frm')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1df362c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _fill_item(item, obj):\n",
    "    if not isinstance(item,FT): return item\n",
    "    tag,cs,attr = item.list\n",
    "    if isinstance(cs,tuple): cs = tuple(_fill_item(o, obj) for o in cs)\n",
    "    name = attr.get('name', None)\n",
    "    val = None if name is None else obj.get(name, None)\n",
    "    if val is not None and not 'skip' in attr:\n",
    "        if tag=='input':\n",
    "            if attr.get('type', '') in ('checkbox','radio'):\n",
    "                if val: attr['checked'] = '1'\n",
    "                else: attr.pop('checked', '')\n",
    "            else: attr['value'] = val\n",
    "        if tag=='textarea': cs=(val,)\n",
    "        if tag == 'select':\n",
    "            option = next((o for o in cs if o.tag=='option' and o.get('value')==val), None)\n",
    "            if option: option.selected = '1'\n",
    "    return FT(tag,cs,attr,void_=item.void_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0c83f26",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def fill_form(form:FT, obj)->FT:\n",
    "    \"Fills named items in `form` using attributes in `obj`\"\n",
    "    if is_dataclass(obj): obj = asdict(obj)\n",
    "    elif not isinstance(obj,dict): obj = obj.__dict__\n",
    "    return _fill_item(form, obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "caef04d9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form><fieldset name=\"stuff\">\n",
       "  <input value=\"Profit\" id=\"title\" class=\"char\" name=\"title\">\n",
       "  <label class=\"px-2\">\n",
       "    <input type=\"checkbox\" name=\"done\" data-foo=\"bar\" class=\"checkboxer\" checked=\"1\">\n",
       "Done\n",
       "  </label>\n",
       "  <input type=\"hidden\" id=\"id\" name=\"id\" value=\"2\">\n",
       "  <select name=\"opt\">\n",
       "    <option value=\"a\"></option>\n",
       "    <option value=\"b\" selected=\"1\"></option>\n",
       "  </select>\n",
       "  <textarea id=\"details\" name=\"details\">Details</textarea>\n",
       "  <button>Save</button>\n",
       "</fieldset>\n",
       "</form>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "form((fieldset((input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'}), label((input((),{'type': 'checkbox', 'name': 'done', 'data-foo': 'bar', 'class': 'checkboxer', 'checked': '1'}), 'Done'),{'class': 'px-2'}), input((),{'type': 'hidden', 'id': 'id', 'name': 'id', 'value': 2}), select((option((),{'value': 'a'}), option((),{'value': 'b', 'selected': '1'})),{'name': 'opt'}), textarea(('Details',),{'id': 'details', 'name': 'details'}), button(('Save',),{})),{'name': 'stuff'}),),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@dataclass\n",
    "class TodoItem:\n",
    "    title:str; id:int; done:bool; details:str; opt:str='a'\n",
    "\n",
    "todo = TodoItem(id=2, title=\"Profit\", done=True, details=\"Details\", opt='b')\n",
    "check = Label(Input(type=\"checkbox\", cls=\"checkboxer\", name=\"done\", data_foo=\"bar\"), \"Done\", cls='px-2')\n",
    "form = Form(Fieldset(Input(cls=\"char\", id=\"title\", value=\"a\"), check, Input(type=\"hidden\", id=\"id\"),\n",
    "                     Select(Option(value='a'), Option(value='b'), name='opt'),\n",
    "                     Textarea(id='details'), Button(\"Save\"),\n",
    "                     name=\"stuff\"))\n",
    "form = fill_form(form, todo)\n",
    "assert '<textarea id=\"details\" name=\"details\">Details</textarea>' in to_xml(form)\n",
    "form"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8b171490",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def fill_dataclass(src, dest):\n",
    "    \"Modifies dataclass in-place and returns it\"\n",
    "    for nm,val in asdict(src).items(): setattr(dest, nm, val)\n",
    "    return dest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77e3f785",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "TodoItem(title='Profit', id=2, done=True, details='Details', opt='b')"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nt = TodoItem('', 0, False, '')\n",
    "fill_dataclass(todo, nt)\n",
    "nt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9594f70",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def find_inputs(e, tags='input', **kw):\n",
    "    \"Recursively find all elements in `e` with `tags` and attrs matching `kw`\"\n",
    "    if not isinstance(e, (list,tuple,FT)): return []\n",
    "    inputs = []\n",
    "    if isinstance(tags,str): tags = [tags]\n",
    "    elif tags is None: tags = []\n",
    "    cs = e\n",
    "    if isinstance(e, FT):\n",
    "        tag,cs,attr = e.list\n",
    "        if tag in tags and kw.items()<=attr.items(): inputs.append(e)\n",
    "    for o in cs: inputs += find_inputs(o, tags, **kw)\n",
    "    return inputs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7e740aac",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'})]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "inps = find_inputs(form, id='title')\n",
    "test_eq(len(inps), 1)\n",
    "inps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04b4f6c7",
   "metadata": {},
   "source": [
    "You can also use lxml for more sophisticated searching:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e551de91",
   "metadata": {},
   "outputs": [],
   "source": [
    "elem = lx.fromstring(to_xml(form))\n",
    "test_eq(elem.xpath(\"//input[@id='title']/@value\"), ['Profit'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d8a28b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def __getattr__(tag):\n",
    "    if tag.startswith('_') or tag[0].islower(): raise AttributeError\n",
    "    tag = tag.replace(\"_\", \"-\")\n",
    "    def _f(*c, target_id=None, **kwargs): return ft_hx(tag, *c, target_id=target_id, **kwargs)\n",
    "    return _f"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afb0f65c",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "_re_h2x_attr_key = re.compile(r'^[A-Za-z_-][\\w-]*$')\n",
    "def html2ft(html, attr1st=False):\n",
    "    \"\"\"Convert HTML to an `ft` expression\"\"\"\n",
    "    rev_map = {'class': 'cls', 'for': 'fr'}\n",
    "    \n",
    "    def _parse(elm, lvl=0, indent=4):\n",
    "        if isinstance(elm, str): return repr(elm.strip()) if elm.strip() else ''\n",
    "        if isinstance(elm, list): return '\\n'.join(_parse(o, lvl) for o in elm)\n",
    "        tag_name = elm.name.capitalize().replace(\"-\", \"_\")\n",
    "        if tag_name=='[document]': return _parse(list(elm.children), lvl)\n",
    "        cts = elm.contents\n",
    "        cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)\n",
    "              for c in cts if str(c).strip()]\n",
    "        attrs = []\n",
    "        for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):\n",
    "            if isinstance(value,(tuple,list)): value = \" \".join(value)\n",
    "            key = rev_map.get(key, key)\n",
    "            attrs.append(f'{key.replace(\"-\", \"_\")}={value!r}'\n",
    "                         if _re_h2x_attr_key.match(key) else f'**{{{key!r}:{value!r}}}')\n",
    "        spc = \" \"*lvl*indent\n",
    "        onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))\n",
    "        j = ', ' if onlychild else f',\\n{spc}'\n",
    "        inner = j.join(filter(None, cs+attrs))\n",
    "        if onlychild: return f'{tag_name}({inner})'\n",
    "        if not attr1st or not attrs: return f'{tag_name}(\\n{spc}{inner}\\n{\" \"*(lvl-1)*indent})' \n",
    "        inner_cs = j.join(filter(None, cs))\n",
    "        inner_attrs = ', '.join(filter(None, attrs))\n",
    "        return f'{tag_name}({inner_attrs})(\\n{spc}{inner_cs}\\n{\" \"*(lvl-1)*indent})'\n",
    "\n",
    "    soup = BeautifulSoup(html.strip(), 'html.parser')\n",
    "    for c in soup.find_all(string=risinstance(Comment)): c.extract()\n",
    "    return _parse(soup, 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3569a964",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```python\n",
       "Form(\n",
       "    Fieldset(\n",
       "        Input(value='Profit', id='title', name='title', cls='char'),\n",
       "        Label(\n",
       "            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n",
       "            'Done',\n",
       "            cls='px-2'\n",
       "        ),\n",
       "        Input(type='hidden', id='id', name='id', value='2'),\n",
       "        Select(\n",
       "            Option(value='a'),\n",
       "            Option(value='b', selected='1'),\n",
       "            name='opt'\n",
       "        ),\n",
       "        Textarea('Details', id='details', name='details'),\n",
       "        Button('Save'),\n",
       "        name='stuff'\n",
       "    )\n",
       ")\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "h = to_xml(form)\n",
    "hl_md(html2ft(h), 'python')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af92d7d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```python\n",
       "Form(\n",
       "    Fieldset(name='stuff')(\n",
       "        Input(value='Profit', id='title', name='title', cls='char'),\n",
       "        Label(cls='px-2')(\n",
       "            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n",
       "            'Done'\n",
       "        ),\n",
       "        Input(type='hidden', id='id', name='id', value='2'),\n",
       "        Select(name='opt')(\n",
       "            Option(value='a'),\n",
       "            Option(value='b', selected='1')\n",
       "        ),\n",
       "        Textarea('Details', id='details', name='details'),\n",
       "        Button('Save')\n",
       "    )\n",
       ")\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hl_md(html2ft(h, attr1st=True), 'python')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "474e14b4",
   "metadata": {},
   "source": [
    "# Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d211e8e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#|hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "814cf69a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
